package com.starview.star.compiler.generate;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;

import com.starview.platform.data.type.DisplayType;
import com.starview.platform.data.type.IAlgebraicType;
import com.starview.platform.data.type.IType;
import com.starview.platform.data.type.Location;
import com.starview.star.compiler.canonical.IContentExpression;
import com.starview.star.compiler.canonical.Variable;
import com.starview.star.compiler.type.BindingKind;
import com.starview.star.compiler.util.AccessMode;
import com.starview.star.compiler.util.ContinueFlag;
import com.starview.star.compiler.util.EntryVisitor;
import com.starview.star.compiler.util.LayeredHash;
import com.starview.star.compiler.util.LayeredMap;
import com.starview.star.compiler.util.PrettyPrintDisplay;
import com.starview.star.compiler.util.PrettyPrintable;
import com.starview.star.compiler.util.UndoManager;
import com.starview.star.compiler.util.UpdateEntry;

@SuppressWarnings("serial")
public class CContext implements PrettyPrintable
{
  private final UndoManager undoMgr = new UndoManager();
  private final LayeredMap<String, DictEntry> dict;
  private final LayeredMap<String, IContentExpression> rewrites;
  private final Map<String, IAlgebraicType> introducedTypes;
  private final CContext outer;
  private final Location loc;

  public CContext(CContext template)
  {
    LayeredMap<String, DictEntry> dict = new LayeredHash<String, DictEntry>();
    for (Entry<String, DictEntry> entry : template.dict.entrySet()) {
      DictEntry decl = entry.getValue();
      DictEntry frEntry = new DictEntry(entry.getKey(), decl.getVariable(), decl.getLoc(), decl.getAccess(),
          BindingKind.free);
      dict.put(entry.getKey(), frEntry);
    }
    this.introducedTypes = template.introducedTypes;
    this.dict = dict.fork();
    this.rewrites = template.rewrites.fork();
    this.outer = template;
    this.loc = template.loc;
  }

  public CContext(Location loc)
  {
    introducedTypes = new HashMap<>();
    dict = new LayeredHash<String, DictEntry>();
    rewrites = new LayeredHash<String, IContentExpression>();
    this.outer = null;
    this.loc = loc;
  }

  public Map<String, IAlgebraicType> getIntroducedTypes()
  {
    return introducedTypes;
  }

  public void introduceType(IAlgebraicType desc)
  {
    if (!introducedTypes.containsKey(desc.getName()))
      introducedTypes.put(desc.getName(), desc);
  }

  public static void introduceGlobalType(CContext cxt, IAlgebraicType desc)
  {
    if (cxt != null) {
      while (cxt.outer != null)
        cxt = cxt.outer;
      cxt.introduceType(desc);
    }
  }

  public IAlgebraicType introducedType(String name)
  {
    return introducedTypes.get(name);
  }

  public Location getLoc()
  {
    return loc;
  }

  public void defineRewrite(String var, IContentExpression rewrite)
  {
    undoMgr.pushUndo(new RewriteUndo(var, rewrites.get(var)));

    rewrites.put(var, rewrite);
  }

  public IContentExpression rewriteVar(String var)
  {
    return rewrites.get(var);
  }

  public void defineFree(String name, IType type, AccessMode readOnly)
  {
    DictEntry entry = new DictEntry(name, Variable.create(loc, type, name), loc, readOnly, BindingKind.free);
    undoMgr.pushUndo(new DictUndo(name, dict.get(name)));
    dict.put(name, entry);
  }

  public void defineLocal(String name, Variable var, AccessMode readOnly)
  {
    undoMgr.pushUndo(new DictUndo(name, dict.get(name)));
    DictEntry entry = new DictEntry(name, var, var.getLoc(), readOnly, BindingKind.local);
    dict.put(name, entry);
  }

  public List<Variable> definedVariables()
  {
    List<Variable> vars = new ArrayList<Variable>();
    for (DictEntry entry : dict.values()) {
      Variable v = entry.getVariable();
      if (!vars.contains(v))
        vars.add(v);
    }
    return vars;
  }

  public boolean isDefined(String name)
  {
    return dict.containsKey(name);
  }

  public DictEntry getDictInfo(String name)
  {
    return dict.get(name);
  }

  public IType getTypeOf(String name)
  {
    assert dict.containsKey(name);

    return dict.get(name).getType();
  }

  public AccessMode isReadOnly(String name)
  {
    DictEntry entry = dict.get(name);
    assert entry != null;
    return entry.access;
  }

  public void resetDict(int reset)
  {
    undoMgr.resetStack(reset);
  }

  public int getMark()
  {
    return undoMgr.getCurrentState();
  }

  public BindingKind varLocation(String name)
  {
    DictEntry entry = dict.get(name);
    if (entry != null)
      return entry.getBindingKind();
    else
      return null;
  }

  public IType typeOf(String name)
  {
    DictEntry entry = dict.get(name);

    if (entry != null)
      return entry.getType();
    else
      return null;
  }

  @Override
  public String toString()
  {
    PrettyPrintDisplay disp = new PrettyPrintDisplay();

    prettyPrint(disp);

    return disp.toString();
  }

  @Override
  public void prettyPrint(final PrettyPrintDisplay disp)
  {
    int mark = disp.markIndent(2);
    disp.append("vars{");

    dict.visit(new EntryVisitor<String, DictEntry>() {

      @Override
      public ContinueFlag visit(String key, DictEntry var)
      {
        disp.appendWord(key);
        disp.append("/");
        disp.append(var.getBindingKind().toString());

        disp.append(" has type ");
        DisplayType.display(disp, var.getType());
        disp.append("\n");

        return ContinueFlag.cont;
      }
    });

    disp.append("}\n");

    disp.popIndent(mark);
  }

  public ContinueFlag visitCContext(EntryVisitor<String, DictEntry> visitor)
  {
    return dict.visit(visitor);
  }

  private class DictUndo extends UpdateEntry
  {
    String name;
    DictEntry old;

    DictUndo(String name, DictEntry old)
    {
      this.name = name;
      this.old = old;
    }

    @Override
    public void reset()
    {
      dict.remove(name);
      if (old != null)
        dict.put(name, old);
    }
  }

  private class RewriteUndo extends UpdateEntry
  {
    String name;
    IContentExpression old;

    RewriteUndo(String name, IContentExpression old)
    {
      this.name = name;
      this.old = old;
    }

    @Override
    public void reset()
    {
      if (old != null)
        rewrites.put(name, old);
      else
        rewrites.remove(name);
    }
  }
}
